/**
 * 
 */
package edu.hm.compiler.parser.derive;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import edu.hm.compiler.I18NCompiler;
import edu.hm.compiler.parser.grammar.BodyOption;
import edu.hm.compiler.parser.grammar.ConstantString;
import edu.hm.compiler.parser.grammar.Head;
import edu.hm.compiler.parser.grammar.Query;
import edu.hm.compiler.parser.grammar.Rule;
import edu.hm.compiler.parser.grammar.RuleSystem;
import edu.hm.compiler.parser.grammar.Symbol;
import edu.hm.compiler.parser.grammar.predicate.Predicate;
import edu.hm.compiler.parser.grammar.predicate.PredicateSymbol;
import edu.hm.compiler.parser.grammar.term.Term;
import edu.hm.compiler.parser.grammar.term.TermCall;
import edu.hm.compiler.parser.grammar.term.TermString;
import edu.hm.compiler.parser.grammar.term.TermSymbol;
import edu.hm.compiler.parser.grammar.term.TermVariable;
import edu.hm.compiler.parser.grammar.variable.AnonymousVariable;
import edu.hm.compiler.parser.grammar.variable.Variable;
import edu.hm.compiler.scanner.IScanner;
import edu.hm.compiler.scanner.token.ErrorToken;
import edu.hm.compiler.scanner.token.IToken;
import edu.hm.compiler.scanner.token.ITokenClass;
import edu.hm.compiler.scanner.token.TokenClass;

/**
 * @author Saruman
 */
public class ASTBuilder
{
    private final TokenClass        whiteSpace           = new TokenClass( "WhiteSpace" ); //$NON-NLS-1$
    private final TokenClass        comment              = new TokenClass( "Comment" );   //$NON-NLS-1$
    private final TokenClass        openingBracket       = new TokenClass( "ParanO" );    //$NON-NLS-1$
    private final TokenClass        closingBracket       = new TokenClass( "ParanC" );    //$NON-NLS-1$
    private final TokenClass        comma                = new TokenClass( "Comma" );     //$NON-NLS-1$
    private final TokenClass        colon                = new TokenClass( "Colon" );     //$NON-NLS-1$
    private final TokenClass        dot                  = new TokenClass( "Dot" );       //$NON-NLS-1$
    private final TokenClass        string               = new TokenClass( "String" );    //$NON-NLS-1$
    private final TokenClass        consId               = new TokenClass( "ConsId" );    //$NON-NLS-1$
    private final TokenClass        varId                = new TokenClass( "VarId" );     //$NON-NLS-1$
    private final TokenClass        underScore           = new TokenClass( "Underscore" ); //$NON-NLS-1$

    private final IScanner          scanner;
    private final Set< TokenClass > tokenClassesToIgnore = new HashSet<>();
    private final ASTPrinter        printer;

    /**
     * @param scanner
     */
    public ASTBuilder( final IScanner scanner, final ASTPrinter printer )
    {
        this.scanner = scanner;
        this.printer = printer;
        this.tokenClassesToIgnore.add( this.comment );
        this.tokenClassesToIgnore.add( this.whiteSpace );
    }

    public ASTBuilder( final IScanner scanner )
    {
        this( scanner, new NoPrinter() );
    }

    public RuleSystem getAST() throws ParserException
    {
        this.printer.out( I18NCompiler.getString( "RuleSystem" ) ); //$NON-NLS-1$
        final Query query = this.deriveQuery();
        final List< Rule > ruleList = this.deriveRuleList();
        return new RuleSystem( query, ruleList );
    }

    private Query deriveQuery() throws ParserException
    {
        this.printer.inc();
        this.printer.out( I18NCompiler.getString( "Query" ) ); //$NON-NLS-1$
        this.checkTokenClass( this.getNextToken(), this.colon, true );
        final List< Predicate > predicateList = this.derivePredicateList();
        final IToken token = this.getNextToken();
        this.checkTokenClass( token, this.dot, true );
        this.printer.dec();
        return new Query( predicateList );
    }

    private List< Predicate > derivePredicateList() throws ParserException
    {
        this.printer.inc();
        this.printer.out( I18NCompiler.getString( "PredicateList" ) ); //$NON-NLS-1$
        final Predicate predicate = this.derivePredicate();
        final List< Predicate > predicateList = this.derivePredicateContinue();
        this.printer.dec();
        predicateList.add( 0, predicate );
        return predicateList;
    }

    private List< Predicate > derivePredicateContinue() throws ParserException
    {
        this.printer.inc();
        this.printer.out( I18NCompiler.getString( "PredicateContinue" ) ); //$NON-NLS-1$
        IToken iToken;
        iToken = this.getNextToken();
        List< Predicate > predicateList;
        if( this.checkTokenClass( iToken, this.comma, false ) )
        {
            final Predicate predicate = this.derivePredicate();
            predicateList = this.derivePredicateContinue();
            predicateList.add( 0, predicate );
        } else
        {
            predicateList = new ArrayList<>();
            this.scanner.unget( iToken );
        }
        this.printer.dec();
        return predicateList;
    }

    private Predicate derivePredicate() throws ParserException
    {
        this.printer.inc();
        this.printer.out( I18NCompiler.getString( "Predicate" ) ); //$NON-NLS-1$
        IToken token = this.getNextToken();
        this.checkTokenClass( token, this.consId, true );
        final Symbol symbol = new Symbol( token.getPattern() );
        token = this.getNextToken();
        List< Term > termList;
        if( this.checkTokenClass( token, this.openingBracket, false ) )
        {
            termList = this.deriveTermlist();
            this.checkTokenClass( this.getNextToken(), this.closingBracket, true );
        } else
        {
            termList = new ArrayList<>();
            this.scanner.unget( token );
        }
        this.printer.dec();
        return new PredicateSymbol( symbol, termList );
    }

    private List< Term > deriveTermlist() throws ParserException
    {
        this.printer.inc();
        this.printer.out( I18NCompiler.getString( "TermList" ) ); //$NON-NLS-1$
        final Term term = this.deriveTerm();
        final List< Term > termList = this.deriveTermContinue();
        this.printer.dec();
        termList.add( 0, term );
        return termList;
    }

    private List< Term > deriveTermContinue() throws ParserException
    {
        this.printer.inc();
        this.printer.out( I18NCompiler.getString( "TermContinue" ) ); //$NON-NLS-1$
        final IToken nextToken = this.getNextToken();
        List< Term > termList;
        if( this.checkTokenClass( nextToken, this.comma, false ) )
        {
            final Term term = this.deriveTerm();
            termList = this.deriveTermContinue();
            termList.add( 0, term );
        } else
        {
            termList = new ArrayList<>();
            this.scanner.unget( nextToken );
        }
        this.printer.dec();
        return termList;
    }

    private Term deriveTerm() throws ParserException
    {
        this.printer.inc();
        this.printer.out( I18NCompiler.getString( "Term" ) ); //$NON-NLS-1$
        Term term;
        IToken token = this.getNextToken();
        if( this.checkTokenClass( token, this.consId, false ) )
        {
            this.printer.out( I18NCompiler.getString( "Symbol" ) ); //$NON-NLS-1$
            final Symbol symbol = new Symbol( token.getPattern() );
            token = this.getNextToken();
            if( this.checkTokenClass( token, this.openingBracket, false ) )
            {
                final List< Term > deriveTermlist = this.deriveTermlist();
                token = this.getNextToken();
                this.checkTokenClass( token, this.closingBracket, true );
                term = new TermCall( symbol, deriveTermlist );
            } else
            {
                term = new TermSymbol( symbol );
                this.scanner.unget( token );
            }
        } else if( this.checkTokenClass( token, this.string, false ) )
        {
            this.printer.out( I18NCompiler.getString( "String" ) ); //$NON-NLS-1$
            term = new TermString( new ConstantString( token.getPattern() ) );
        } else if( this.checkTokenClass( token, this.varId, false ) )
        {
            this.printer.out( I18NCompiler.getString( "VarId" ) ); //$NON-NLS-1$
            term = new TermVariable( new Variable( token.getPattern() ) );
        } else if( this.checkTokenClass( token, this.underScore, false ) )
        {
            this.printer.out( I18NCompiler.getString( "AnonymousVar" ) ); //$NON-NLS-1$
            term = new TermVariable( new AnonymousVariable() );
        } else
            throw new ParserException( token, I18NCompiler.getString(
                    "UnexpectedToken", token.getTokenClass().getName() ) ); //$NON-NLS-1$
        this.printer.dec();
        return term;
    }

    private List< Rule > deriveRuleList() throws ParserException
    {
        this.printer.inc();
        this.printer.out( I18NCompiler.getString( "RuleList" ) ); //$NON-NLS-1$
        final Rule rule = this.deriveRule();
        final List< Rule > ruleList = this.deriveRuleContinue();
        this.printer.dec();
        ruleList.add( 0, rule );
        return ruleList;
    }

    private List< Rule > deriveRuleContinue() throws ParserException
    {
        this.printer.inc();
        this.printer.out( I18NCompiler.getString( "RuleContinue" ) ); //$NON-NLS-1$
        List< Rule > ruleList;
        final IToken nextToken = this.getNextToken();
        if( !this.checkTokenClass( nextToken, ErrorToken.EOF_TOKEN_CLASS, false ) )
        {
            this.scanner.unget( nextToken );
            final Rule rule = this.deriveRule();
            ruleList = this.deriveRuleContinue();
            ruleList.add( 0, rule );
        } else
        {
            this.scanner.unget( nextToken );
            ruleList = new ArrayList<>();
        }
        this.printer.dec();
        return ruleList;
    }

    private Rule deriveRule() throws ParserException
    {
        this.printer.inc();
        this.printer.out( I18NCompiler.getString( "Rule" ) ); //$NON-NLS-1$
        final Head head = this.deriveHead();
        final BodyOption bodyOption = this.deriveBodyOption();
        this.checkTokenClass( this.getNextToken(), this.dot, true );
        this.printer.dec();
        return new Rule( head, bodyOption );
    }

    private BodyOption deriveBodyOption() throws ParserException
    {
        this.printer.inc();
        this.printer.out( I18NCompiler.getString( "BodyOption" ) ); //$NON-NLS-1$
        final IToken nextToken = this.getNextToken();
        List< Predicate > predicateList;
        if( this.checkTokenClass( nextToken, this.colon, false ) )
            predicateList = this.derivePredicateList();
        else
        {
            this.scanner.unget( nextToken );
            predicateList = new ArrayList<>();
        }
        this.printer.dec();
        return new BodyOption( predicateList );
    }

    private Head deriveHead() throws ParserException
    {
        this.printer.inc();
        this.printer.out( I18NCompiler.getString( "Head" ) ); //$NON-NLS-1$
        final Predicate predicate = this.derivePredicate();
        this.printer.dec();
        return new Head( predicate );
    }

    // ------------------------------------------------------------------------------------

    private boolean checkTokenClass( final IToken token, final ITokenClass tClass, final boolean strict )
            throws ParserException
    {
        if( !token.getTokenClass().equals( tClass ) )
        {
            if( strict )
            {
                final String errMsg = I18NCompiler.getString( "Error", token.getLine(), token.getCharPos(),
                        tClass.getName(), token.getPattern() );
                this.printer.err( errMsg );
                throw new ParserException( token, errMsg );
            }
            return false;
        }
        return true;
    }

    private IToken getNextToken() throws ParserException
    {
        try
        {
            IToken token;
            do
                token = this.scanner.get();
            while( this.tokenClassesToIgnore.contains( token.getTokenClass() ) );
            return token;
        } catch( final IOException e )
        {
            throw new ParserException( e );
        }
    }
}
